'''
Created on 09.08.2011

@author: osman
'''

from opf.exceptions import OPFError
from opf.context import setContext, getContext, Context, noChecks
from opf.atomicpredicate import PredicateBase

class StatementArgTypeError(OPFError): pass

class _DummyInstance:
    def __init__(self, name, const = None, isRole = False):
        self.name = name
        self.const = const
        self.isRole = isRole

_tspo_tab = {0:'t', 1:'s', 2:'p', 3:'o'}

class _SkolemVar:
    def __init__(self, cls): self.klass = cls

def exists(cls): return _SkolemVar(cls)

class _ArcContainer(Context):
    def __init__(self):
        # curr is position of an arc in conjunction:
        self.curr = 0
        # keys() of 'links' are names of roles, values() are lists of pairs
        # (arcpos, arcrole), where arcpos is position of an arc in conjunction
        # and arcrole is one of 't', 's', 'p', 'o': 
        self.links = {}
        self.positions = {}
        self.instances = {}
        self.predicates = {}
        self.constants = set()
    def _addArc(self, arc):
        if len(arc) == 3:
            arc = (0, arc[0], arc[1], arc[2])
        t, s, p, o = arc
        t = self.instances.setdefault(id(t),
            _DummyInstance("topic%d" % self.curr))
        p = self.predicates.setdefault(id(p),
            _DummyInstance("predicate%d" % self.curr, id(p)))
        if not isinstance(s, _DummyInstance):
            s = self.instances.setdefault(id(s),
                _DummyInstance("subject%d" % self.curr, id(s)))
        if not isinstance(o, _DummyInstance):
            o = self.instances.setdefault(id(o),
                _DummyInstance("object%d" % self.curr, id(o)))
        arc = t, s, p, o
        for n, inst in enumerate(arc):
            lst = self.positions.setdefault(inst, [])
            lst.append((self.curr, _tspo_tab[n]))
        self.curr += 1
    def _makeLinks(self):
        for inst, posList in self.positions.items():
            self.links[inst.name] = posList
            if inst.const is not None:
                self.constants.add((posList[0], inst.const))
    def _prohibitConsistencyChecks(self): return True

_me = lambda x, y: x + '=' + y
class Predicate(PredicateBase):
    def __init__(self, func):
        c = func.__code__
        self._roleNames = c.co_varnames[:c.co_argcount]
        annotations = func.__annotations__
        self.__func = func
        self.__name__ = annotations.get('return', func.__name__)
        self.__roleTypes = dict((name, annotations.get(name)) for name in
                                self._roleNames)
        # fixed order of roles:
        self.__order = []
        self.__calcLinks()
        self.__selectBase = self.__makeSelectBase()
    def __calcLinks(self):
        ac = _ArcContainer()
        prev = getContext()
        setContext(ac)
        try:
            kwargs = {}
            for rn in self._roleNames:
                inst = _DummyInstance(rn, isRole = True)
                kwargs[rn] = inst
                self.__order.append(rn)
            self.__func(**kwargs)
            ac._makeLinks()
            self.__links = ac.links
            self.__const = ac.constants
            self.__num = ac.curr
        finally: setContext(prev)
    def __makeSelectBase(self):
        # form tablanames: t1, t2, t3...
        tablenames = dict((n, "t" + str(n)) for n in range(self.__num))
        # form couples like (1, 's') meaning that subject in the
        # 1-st arc corresponds to role with name 'roleName': 
        postr = [self.__links[roleName][0] for roleName in self.__order]
        # form result names like t1.s, t2.p ...
        resnames = [tablenames[n[0]] + '.' + n[1] for n in postr]
        fromnames = ", ".join('tspo ' + n for n in tablenames.values())
        wherestats = set('opfct(%s,"%s")' % (x, y) for x, y in
                         zip(resnames, self.__order))
        resnames = ", ".join(resnames)
        for roleName, posList in self.__links.items():
            for i, ipos in enumerate(posList):
                for jpos in posList[i+1:]:
                    # skip conditions on topics:
                    if ipos[1] == 't' and jpos[1] == 't': continue
                    wherestats.add(_me(tablenames[ipos[0]]+'.'+ipos[1],
                                    tablenames[jpos[0]]+'.'+jpos[1]))
        for (n, tp), value in self.__const:
            wherestats.add(_me(tablenames[n] + '.' + tp, str(value)))
        self.__typetab = dict((k, isinstance(v, _SkolemVar) and v.klass or v)
                              for k, v in self.__roleTypes.items()
                              if v is not None)
        return tablenames, resnames, fromnames, wherestats
    def makeSelect(self, **kwargs):
        tablenames, resnames, fromnames, wherestats = self.__selectBase
        where = set(wherestats)
        self._checkRoleNames(kwargs.keys())
        for roleName, val in kwargs.items():
            n, tp = self.__links[roleName][0]
            where.add(_me(tablenames[n] + '.' + tp, str(id(val))))
        where = " and ".join(list(where))
        return resnames, fromnames, where
    def makeDelete(self, **kwargs):
        tablenames, _, fromnames, wherestats = self.__selectBase
        where = set(wherestats)
        self._checkRoleNames(kwargs.keys())
        for roleName, val in kwargs.items():
            n, tp = self.__links[roleName][0]
            where.add(_me(tablenames[n] + '.' + tp, str(id(val))))
        where = " and ".join(list(where))
        return fromnames, where
    def __repr__(self): return self.__name__
    def __call__(self, *args, **kwargs):
        if args:
            kwargs = dict(zip(self._roleNames, args))
        prohibitChecks = noChecks()
        if not prohibitChecks:
            for k, v in kwargs.items():
                rt = self.__roleTypes[k]
                rt = isinstance(rt, _SkolemVar) and rt.klass or rt
                if not isinstance(v, rt):
                    raise StatementArgTypeError(statement = self, argument = k,
                                                actualType = type(v),
                                                typeMustBe = self.__roleTypes[k])
        self.__func(**kwargs)
    def findInGraph(self, graph, **kwargs):
        sel = self.makeSelect(**kwargs)
        for result in graph.select(*sel, typetab = self.__typetab):
            tab = {'predicate': self}
            for rn, v in zip(self.__order, result):
                if isinstance(self.__roleTypes.get(rn), _SkolemVar):
                    continue
                tab[rn] = v
            yield tab
    def deleteFromGraph(self, graph, **kwargs):
        # TODO: this function does not work since graph.delete doesn't
        return graph.delete(*self.makeDelete(**kwargs), typetab = self.__typetab)

if __name__ == '__main__':
    # simple tests:
    import opf.ontology as opfo
    numbers = opfo.Ontology() 
    @numbers
    class Number:
        def __init__(self, v): self.val = v
        def __repr__(self): return 'number<%d>' % self.val
    @numbers.atomic
    def follows(x: Number, y: Number):
        return y.val == x.val + 1
    zz = Number(2)
    def indFollows(x: Number, y: exists(Number), z: Number):
        return (follows(x, y), follows(y, z))
    p = Predicate(indFollows)
    print(p.makeSelect())
    print(p.makeSelect(y = zz))
    a = Number(1)
    b = Number(3)
    with numbers:
        p(x = a, y = zz, z = b)
    for x in p.findInGraph(numbers.graph, x = a):
        print(x)
    print(len(list(p.findInGraph(numbers.graph, x = a))))
